开发环境:Keil 5.30
MCU:STM32F103ZET6
上一章通过控制GPIO的高低电平实现了流水灯,但只是告诉了大家怎么做,如何实现流水灯,本文将深入剖析的GPIO流水灯的前生今世,深入研究流水灯的调用逻辑和数据结构。
2.1 GPIO配置概述
前面一章一个大概讲解GPIO的配置过程和核心的寄存器,当然啦,关于GPIO的寄存器远不止我上一章列出来的,还有很多,具体请参看《STM32F10XXX参考手册》中GPIO相关的内容吧。
根据前面实现的GPIO流水灯,本文将其归纳如下:
要想控制3个LED依次亮灭,就需要做以上三件事:使能时钟,配置GPIO参数,最后循环控制GPIO的高低电平就能实现流水灯的效果,GPIO的寄存器这里就不说了,更多详细的寄存器描述看官方手册就行,下面先来看看STM32的时钟。
2.2 Cortex-M的时钟系统
2.2.1 Cortex-M的系统架构
基于Cortex-M的系统架构比51单片机强大很多了。首先我们看看Cortex-M3的系统架构图:
对于对比,在看看GD32F407xx的系统架构图。
GD32F407xx是基于Cortex-M4设计的。可以看到,Cortex-M3和Cortex-M4大体是相同的,系统主要由四个驱动单元和四个被动单元构成。
四个驱动单元
内核 DCode 总线;
系统总线;
通用 DMA1(DMA0);
通用 DMA2(DMA1);
四被动单元
AHB 到 APB 的桥:连接所有的 APB 设备;
内部 FlASH 闪存;
内部 SRAM;
FSMC;
下面我们具体讲解一下图中几个总线的知识:
① ICode 总线:该总线将 M3 内核指令总线和闪存指令接口相连,指令的预取在该总线上面完成。
② DCode 总线:该总线将 M3 内核的 DCode 总线与闪存存储器的数据接口相连接,常量加载和调试访问在该总线上面完成。
③ 系统总线:该总线连接 M3 内核的系统总线到总线矩阵,总线矩阵协调内核和 DMA 间访问。
④ DMA 总线:该总线将 DMA 的 AHB 主控接口与总线矩阵相连,总线矩阵协调 CPU 的DCode 和 DMA 到 SRAM,闪存和外设的访问。
⑤ 总线矩阵:总线矩阵协调内核系统总线和 DMA 主控总线之间的访问仲裁,仲裁利用轮换算法。
⑥ AHB/APB 桥:这两个桥在 AHB 和 2 个 APB 总线间提供同步连接, APB1 操作速度限于36MHz,APB2 操作速度全速。
对于系统架构的知识,在刚开始学习 STM32 的时候只需要一个大概的了解,大致知道是个什么情况即可。
2.2.2 STM32时钟架构
时钟是整个处理器运行的基础,时钟信号推动处理器内各个部分执行相应的指令。时钟系统就是CPU的脉搏,决定CPU速率,像人的心跳一样 只有有了心跳,人才能做其他的事情,而单片机有了时钟,才能够运行执行指令,才能够做其他的处理 (点灯,串口,ADC),时钟的重要性不言而喻。
我们在学习51单片机时,其最小系统必有晶振电路,这块电路就是单片机的时钟来源,晶振的振荡频率直接影响单片机的处理速度。STM32相比51单片机就复杂得多,不仅是外设非常多,就连时钟来源就有四个。但我们实际使用的时候只会用到有限的几个外设,使用任何外设都需要时钟才能启动,但并不是所有的外设都需要系统时钟那么高的频率,为了兼容不同速度的设备,有些高速,有些低速,如果都用高速时钟,势必造成浪费,而且,同一个电路,时钟越快功耗越快,同时抗电磁干扰能力也就越弱,所以较为复杂的MCU都是采用多时钟源的方法来解决这些问题,因此便有了STM32的时钟系统和时钟树。
STM32三个不同的时钟源可以用来驱动系统时钟(SYSCLK):
● HSI晶振时钟(高速内部时钟信号)
● HSE晶振时钟(高速外部时钟信号)
● PLL时钟
STM32有两个二级时钟源:
● 40kHz的低速内部RC,它可以驱动独立看门狗,还可选择地通过程序选择驱动RTC。 RTC用于从停机/待机模式下自动唤醒系统。
● 32.768kHz的低速外部晶振,可选择它用来驱动RTC(RTCCLK)。
每个时钟源在不使用时都可以单独被打开或关闭,这样就可以优化系统功耗。
当使用HSI作为PLL时钟的输入时,所能达到的最大系统时钟为64MHz。
2.2.3 STM32时钟硬件电路
2.2.3.1 HSE时钟
高速外部时钟信号(HSE)由以下两种时钟源产生:
● HSE外部晶体 / 陶瓷 谐振器(见下图(a))
● HSE用户外部时钟(见下图(b))
(a)外部时钟 (b)晶振时钟
1.外部时钟源(HSE旁路)
在这种模式下,必须提供一个外部时钟源。它的频率可高达25MHz。外部时钟信号(占空比为50%的方波、 正弦波或三角波)必须连到OSC_IN引脚,同时保证OSC_OUT引脚悬空,见图7(a)。这个外部时钟源是指从其他处理器等引入的时钟源,STM32的demo板就是使用的这种方式,主控器MCU的外部时钟源来自ST Link处理器提供的时钟信号。
2.外部晶体 / 陶瓷谐振器(HSE晶体)
这个4~16MHz的外部晶振的优点在于能产生非常精确的主时钟。 图8显示了它需要的相关硬件配置。谐振器和负载电容需要尽可能近地靠近振荡器的引脚,以减小输出失真和启动稳定时间。负载电容值必须根据选定的晶振进行调节。这种方式也是我们常用的方式,具体电路如下所示。
2.2.3.2 LSE时钟
低速外部时钟源(LSE)可以由两个可能的时钟源来产生:
● LSE外部晶体 / 陶瓷谐振器(见图9(a))
● LSE用户外部时钟(见图9(b))
1.外部源(LSE 旁路)
在这种模式下,必须提供一个外部时钟源。它的频率必须为32.768kHz。外部信号(占空比为50%的方波、 正弦波或三角波)必须连到OSC32_IN引脚,同时保证OSC_OUT引脚悬空。
2.外部晶体 / 陶瓷谐振器(LSE晶体)
这个LSE晶体是一个32.768kHz的低速外部晶体或陶瓷谐振器。它的优点在于能为实时时钟部件(RTC)提供一个低速的,但高精确的时钟源。 RTC可以用于时钟/日历或其它需要计时的场合。谐振器和加载电容需要尽可能近地靠近晶振引脚,这样能使输出失真和启动稳定时间减到最小。负载电容值必须根据选定的晶振进行调节。外部晶体时钟如下图所示。
(a)外部时钟 (b)晶振时钟
HSE和LSE外部晶体两时钟电路的两个电容式为了抗干扰。对抗自然界中的一些干扰,如雷击。
2.2.4 STM32的时钟系统
STM32 芯片为了实现低功耗,设计了一个功能完善但却非常复杂的时钟系统。普通的MCU 一般只要配置好 GPIO 的寄存器就可以使用了,但 STM32 还有一个步骤,就是开启外设时钟。
在 STM32 中,可分为五种时钟源,为 HSI、 HSE、 LSI、 LSE、 PLL。 从时钟频率来分可以分为高速时钟源和低速时钟源,其中 HIS, HSE 以及 PLL 是高速时钟, LSI 和 LSE 是低速时钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中 HSE 和 LSE 是外部时钟源,其他的是内部时钟源。下面我们看看 STM32 的 5 个时钟源,我们讲解顺序是按图中红圈标示的顺序:
①HSI 是高速内部时钟, RC 振荡器,频率为 8MHz。
②HSE 是高速外部时钟,可接石英 /陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~16MHz。 我们的开发板接的是 8M 的晶振。当使用有源晶振时,时钟从 OSC_IN 引脚进入, OSC_OUT 引脚悬空,当选用无源晶振时,时钟从 OSC_IN 和 OSC_OUT 进入,并且要配谐振电容。HSE 最常使用的就是 8M 的无源晶振。当确定 PLL 时钟来源的时候, HSE 可以不分频或者 2 分频,这个由时钟配置寄存器 CFGR 的位 17。
③LSI 是低速内部时钟,RC 振荡器,频率为 40kHz。独立看门狗的时钟源只能是 LSI,同时 LSI 还可以作为 RTC 的时钟源。
④LSE 是低速外部时钟,接频率为 32.768kHz 的石英晶体。这个主要是 RTC 的时钟源。
⑤PLL 为锁相环倍频输出,其时钟输入源可选择为 HSI/2、HSE 或者 HSE/2。倍频可选择为2~16 倍,但是其输出频率最大不得超过 72MHz。
图中我们用 A~E 标示我们要讲解的地方。
A. MCO 是 STM32 的一个时钟输出 IO(PA8),它可以选择一个时钟信号输出, 可以选择为 PLL 输出的 2 分频、 HSI、 HSE、或者系统时钟。这个时钟可以用来给外部其他系统提供时钟源。
B. 这里是 RTC 时钟源,从图上可以看出, RTC 的时钟源可以选择 LSI, LSE,以及HSE 的 128 分频。
C. 从图中可以看出 C 处 USB 的时钟是来自 PLL 时钟源。 STM32 中有一个全速功能的 USB 模块,其串行接口引擎需要一个频率为 48MHz 的时钟源。该时钟源只能从 PLL 输出端获取,可以选择为 1.5 分频或者 1 分频,也就是,当需要使用 USB模块时, PLL 必须使能,并且时钟频率配置为 48MHz 或 72MHz。
D. D 处就是 STM32 的系统时钟 SYSCLK,它是供 STM32 中绝大部分部件工作的时钟源。系统时钟可选择为 PLL 输出、 HSI 或者 HSE。系统时钟最大频率为 72MHz,当然你也可以超频,不过一般情况为了系统稳定性是没有必要冒风险去超频的。
E. 这里的 E 处是指其他所有外设了。从时钟图上可以看出,其他所有外设的时钟最终来源都是 SYSCLK。 SYSCLK 通过 AHB 分频器分频后送给各模块使用。这些模块包括:
①AHB 总线、内核、内存和 DMA 使用的 HCLK 时钟。
②通过 8 分频后送给 Cortex 的系统定时器时钟,也就是 systick 了。
③直接送给 Cortex 的空闲运行时钟 FCLK。
④送给 APB1 分频器。 APB1 分频器输出一路供 APB1 外设使用(PCLK1,最大频率 36MHz),另一路送给定时器(Timer)2、 3、 4 倍频器使用。
⑤送给 APB2 分频器。 APB2 分频器分频输出一路供 APB2 外设使用(PCLK2,最大频率 72MHz),另一路送给定时器(Timer)1 倍频器使用。
其中需要理解的是 APB1 和 APB2 的区别, APB1 上面连接的是低速外设,包括电源接口、备份接口、 CAN、 USB、 I2C1、 I2C2、 UART2、 UART3 等等, APB2 上面连接的是高速外设包括 UART1、 SPI1、 Timer1、 ADC1、 ADC2、所有普通 IO 口(PA~PE)、第二功能 IO 口等。
不同的总线有不同的频率,不同的外设挂在不同的总线下,为了更适合初学者查阅,笔者把常用的外设与总线的对应关系总结如下:
SystemInit()函数中设置的系统时钟大小:
SYSCLK(系统时钟) =72MHz
AHB 总线时钟(使用 SYSCLK) =72MHz
APB1 总线时钟(PCLK1) =36MHz
APB2 总线时钟(PCLK2) =72MHz
PLL 时钟 =72MHz
具体代码请读者查看工程文件的system_stm32f10x.c文件。
举个例子:Keil编写程序是默认的时钟为72Mhz,其实是这么来的:外部晶振(HSE)提供的8MHz(与电路板上的晶振的相关)通过PLLXTPRE分频器后,进入PLLSRC选择开关,进而通过PLLMUL锁相环进行倍频(x9)后,为系统提供72MHz的系统时钟(SYSCLK)。之后是AHB预分频器对时钟信号进行分频,然后为低速外设提供时钟。
或者内部RC振荡器(HSI) 为8MHz /2 为4MHz 进入PLLSRC选择开关,通过PLLMUL锁相环进行倍频(x18)后 为72MHz。
PS: 网上有很多人说是5个时钟源,这种说法有点问题,学习之后就会发现PLL并不是自己产生的时钟源,而是通过其他三个时钟源倍频得到的时钟,这点在前文已近讲解得很清楚了。
2.2.5 STM32的时钟配置剖析
既然时钟搞清楚了,接下来回到上一章的配置时钟的代码:
/*开启LED的外设时钟*/
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOG, ENABLE);
RCC_APB2PeriphClockCmd就是配置时钟的函数,函数原型如下:
/**
* @brief Enables or disables the High Speed APB (APB2) peripheral clock.
* @param RCC_APB2Periph: specifies the APB2 peripheral to gates its clock.
* This parameter can be any combination of the following values:
* @arg RCC_APB2Periph_AFIO, RCC_APB2Periph_GPIOA, RCC_APB2Periph_GPIOB,
* RCC_APB2Periph_GPIOC, RCC_APB2Periph_GPIOD, RCC_APB2Periph_GPIOE,
* RCC_APB2Periph_GPIOF, RCC_APB2Periph_GPIOG, RCC_APB2Periph_ADC1,
* RCC_APB2Periph_ADC2, RCC_APB2Periph_TIM1, RCC_APB2Periph_SPI1,
* RCC_APB2Periph_TIM8, RCC_APB2Periph_USART1, RCC_APB2Periph_ADC3,
* RCC_APB2Periph_TIM15, RCC_APB2Periph_TIM16, RCC_APB2Periph_TIM17,
* RCC_APB2Periph_TIM9, RCC_APB2Periph_TIM10, RCC_APB2Periph_TIM11
* @param NewState: new state of the specified peripheral clock.
* This parameter can be: ENABLE or DISABLE.
* @retval None
*/
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)
{
/* Check the parameters */
assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph));
assert_param(IS_FUNCTIONAL_STATE(NewState));
if (NewState != DISABLE)
{
RCC->APB2ENR |= RCC_APB2Periph;
}
else
{
RCC->APB2ENR &= ~RCC_APB2Periph;
}
}
第一个参数就是具体的外设时钟,第二参数就是时钟状态开关,整个函数很简单,就是配置RCC->APB2ENR,assert_param是检查参数的,关于assert_param详细讲解看附录的小贴士。
参数RCC_APB2Periph传入值是通过宏来定义的,这样的好处也是便于移植,如果换了MCU,架构一样,只需要就该底层驱动就行,不需要更改上层应用,这样就提高了开发效率。言归正传,我们传入的RCC_APB2Periph_GPIOB和RCC_APB2Periph_GPIOG定义如下。
其实就是一个个偏移而已。我们还是看看RCC_APB2ENR寄存器,
如果要配置GPIOB的时钟就是要将第3位置1,因此转换成10进制就是8,同理GPIOG也是一样的。
RCC_APB2ENR的地址是0x18,更准确的是说偏移地址是0x18,在代码中也是有体现的,我们看看RCC结构体吧。
#define RCC ((RCC_TypeDef *) RCC_BASE)
这里是通过宏的方式定义的,结构体就是RCC_TypeDef。
/**
* @brief Reset and Clock Control
*/
typedef struct
{
__IO uint32_t CR;
__IO uint32_t CFGR;
__IO uint32_t CIR;
__IO uint32_t APB2RSTR;
__IO uint32_t APB1RSTR;
__IO uint32_t AHBENR;
__IO uint32_t APB2ENR;
__IO uint32_t APB1ENR;
__IO uint32_t BDCR;
__IO uint32_t CSR;
#ifdef STM32F10X_CL
__IO uint32_t AHBRSTR;
__IO uint32_t CFGR2;
#endif /* STM32F10X_CL */
#if defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || defined (STM32F10X_HD_VL)
uint32_t RESERVED0;
__IO uint32_t CFGR2;
#endif /* STM32F10X_LD_VL || STM32F10X_MD_VL || STM32F10X_HD_VL */
} RCC_TypeDef;
这里可以看到有IO修饰结构体成员,关于IO详细讲解看附录的小贴士。RCC_TypeDef中的成员都是32位,因此偏移都是4,而APB2ENR的偏移量就是寄存器表的偏移一一对应了,其他的结构体都是这样定义的。
在说说RCC_BASE,这就实际的RCC的物理地址,其中的依赖关系如下:
#define PERIPH_BASE ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)
这样我们就知道最终的RCC的基地址是0x0x40021000。这个地址也可以在参考手册中找到。
是不就都对上了,MCU所有的外设都对应了一个地址,因此通过操作该地址就能控制相应的功能,关于STM32的存储管理在后文会详细描述,这里先了解个大概就好。
值得注意的是,既然有RCC_APB2PeriphClockCmd函数,肯定还有RCC_APB1PeriphClockCmd,它们各自配置的时钟可以从系统架构中看到,当然啦,函数的说明中也列举了具体的外设。
到这里基本上时钟的配置就完成了。但是我想还是有很多朋友没有很明白,下面就GPIO的配置从地址映射和固件库的封装两个方便再来详细总结下固件库是如何完成GPIO的具体操作的。
2.3 Cortex-M的地址映射
我们先看看51 单片机中是怎么做的,51 单片机开发中会引用一个 reg51.h 的头文件,51单片机是通过以下方式将名字和寄存器联系起来的:
sfr P0 =0x80;
sfr 也是一种扩充数据类型,占用一个内存单元,值域为 0~255。利用它可以访问 51 单片机内部的所有特殊功能寄存器。如用 sfr P1 = 0x90 这一句定义 P1 为 P1 端口在片内的寄存器。然后我们往地址为 0x80 的寄存器设值的方法是: P0=value;通过改变value的值来控制单片机。
所谓地址映射,就是将芯片上的存储器甚至 I/O 等资源与地址建立一一对应的关系。如果某地址对应着某寄存器,我们就可以运用 C 语言的指针来寻址并修改这个地址上的内容,从而实现修改该寄存器的内容。Cortex-M的地址映射也是类似的。Cortex-M有 32 根地址线,所以它的寻址空间大小为 2 32 bit=4 GB。ARM 公司设计时,预先把这 4 GB 的寻址空间大致地分配好了。它把从 0x40000000 至 0x5FFFFFFF( 512 MB)的地址分配给片上外设。通过把片上外设的寄存器映射到这个地址区,就可以简单地以访问内存的方式,访问这些外设的寄存器,从而控制外设的工作。这样,片上外设可以使用 C 语言来操作。
stm32f10x.h 这个文件中重要的内容就是把 STM32 的所有寄存器进行地址映射。如同51 单片机的 <reg51.h> 头文件一样,stm32f10x.h 像一个大表格,我们在使用的时候就是通过宏定义进行类似查表的操作,但是这样操作会很麻烦,而且32位的MCU寄存器很多,非常不方便。于是就有了现在的固件库。
在这里我们以流水灯中的 GPIOB 为例进行剖析,如果是其他的 IO 端口,则改成相应的地址即可。在这个文件中一系列宏实现了地址映射。
#define GPIOB_BASE (APB2PERIPH_BASE + 0xC00)
#define APB2PERIPH_BASE (PERIPH_BASE + 0xC00
#define PERIPH_BASE ((uint32_t)0x40000000)
这几个宏定义是从文件中的几个部分抽离出来的,具体的内容读者可参考stm32f10x.h 源码。
首先看到 PERIPH_BASE 这个宏,宏展开为 0x40000000,并把它强制转换为 uint32_t的 32 位类型数据,这是因为 STM32 的地址是 32 位的,0x40000000 这个地址是 Cortex-M3 核分配给片上外设 512MB 寻址空间中的第一个地址,我们把0x40000000 称为外设基地址。
接下来是宏 APB2PERIPH_BASE,宏展开为 PERIPH_BASE(外设基地址)加上偏移地址 0x10000,即指向的地址为 0x40010000。这个 APB2PERIPH_BASE 宏是什么地址呢?STM32 不同的外设是挂载在不同的总线上的。STM32 芯片有 AHB 总线、APB2总线和 APB1 总线,挂载在这些总线上的外设有特定的地址范围。
其中像 GPIO、串口 1、ADC 及部分定时器是挂载在称为 APB2 的总线上,挂载到APB2 总 线上的外设地址空间是从0x40010000 至 0x40013FFF地址。这里的第一个地址,也就是 0x40010000,称为 APB2PERIPH_BASE (APB2 总线外设基地址)。
而 APB2 总线基地址相对于外设基地址的偏移量为 0x10000 个地址,即为 APB2 相对外设基地址的偏移地址,见下表。
由这个表我们可以知道,stm32f10x.h 这个文件中必然含有用于定义总线外设基地址的宏。
#define APB2PERIPH_BASE PERIPH_BASE
因为偏移量为零,所以 APB2的地址直接就等于外设基地址。
最后到了宏 GPIOB_BASE,宏展开为 APB2PERIPH_BASE (APB2 总线外设的基地址)加上相对 APB2 总线外设基地址的偏移量 0xC00 得到了 GPIOB端口的寄存器组的基地址。这个所谓的 寄存器组又是什么呢?它包括什么寄存器?
细看 stm32f10x.h 文件,我们还可以发现有关各个 GPIO 基地址的宏。
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
除了 GPIOB寄存器组的地址,还有 GPIOA、GPIOC和 GPIOD 的地址,并且这些地址是不一样的。前面提到,每组 GPIO 都对应着独立的一组寄存器,查看 STM32 的数据手册。
注意到这个说明中有一个偏移地址:0x04,这里的偏移地址是相对哪个地址的偏移呢?下面进行举例说明。
对于GPIOB组的寄存器,GPIOB含有的端口配置高寄存器(GPIOB_CRH地址为:GPIOB_BASE +0x04。假如是 GPIOA 组的寄存器,则 GPIOA 含有的端口配置高寄存器(GPIOA_CRH)地址为 :GPIOA_BASE+0x04。也就是说,这个偏移地址,就是该寄存器相对所在寄存器组基地址的偏移量。
2.4固件库对寄存器的封装
ST的工程师用结构体的形式封装了寄存器组,在 stm32f10x.h 文件定义的。
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
有了这些宏,我们就可以定位到具体的寄存器地址,结构体GPIO_TypeDef在 stm32f10x.h 文件中定义的。
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;
结构体内又定义了 7 个 __ IO uint32_t 类型的变量。这些变量都是 32 位,即每个变量占内存空间 4 个字节。在 C 语言中,结构体内变量的存储空间是连续的,也就是说假如我们定义了一个 GPIO_TypeDef ,这个结构体的首地址(变量 CRL 的 地址)若为 0x40011000,那么结构体中第二个变量(CRH)的地址即为 0x40011000 +0x04 , 加上的0x04 正是代表 4 个字节地址的偏移量。
0x04 偏移量正是 GPIOx_CRH 寄存器相对于所在寄存器组的偏移地址。同理,GPIO_TypeDef 结构体内其他变量的偏移量,也与相应的寄存器偏移地址相符。于是,只要我们匹配了结构体的首地址,就可以确定各寄存器的具体地址了。
GPIOA_BASE 在前文已解析,是一个代表 GPIOA组寄存器的基地址。“(GPIO_TypeDef *)”在这里的作用则是把 GPIOA_BASE 地址转换为 GPIO_TypeDef结构体指针类型。有了这样的宏,以后我们写代码的时候, 如果要修改GPIO 的寄存器,就可用修改以下代码的方式来实现。
GPIO_TypeDef * GPIOx; //定义一个 GPIO_TypeDef 型结构体指针 GPIOx
GPIOx = GPIOA; //把指针地址设置为宏 GPIOA 地址
GPIOx->CRL = 0xffffffff; //通过指针访问并修改 GPIOA_CRL 寄存器
通过类似的方式,我们就可以给具体的寄存器写上适当的参数以控制 STM32 了。
这样我们就可以通过库函数实现了GPIO的初始化了。
/**
* @brief 初始化LED的GPIO
* @param None
* @retval None
*/
void LED_GPIO_Config(void)
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*开启LED的外设时钟*/
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOG, ENABLE);
/*设置IO口*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //设置引脚模式为通用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //设置引脚速率为50MHz
/*调用库函数,初始化GPIOB0*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //选择要控制的GPIOB引脚
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;/*选择要控制的引脚*/
GPIO_Init(GPIOG, &GPIO_InitStructure);
/* 开启所有led灯 */
GPIO_SetBits(GPIOB, GPIO_Pin_0);
GPIO_SetBits(GPIOG, GPIO_Pin_6|GPIO_Pin_7);
}
当然啦,上述代码包含了时钟的使能。
GPIO_Init()函数如下:
/**
* @brief Initializes the GPIOx peripheral according to the specified
* parameters in the GPIO_InitStruct.
* @param GPIOx: where x can be (A..G) to select the GPIO peripheral.
* @param GPIO_InitStruct: pointer to a GPIO_InitTypeDef structure that
* contains the configuration information for the specified GPIO peripheral.
* @retval None
*/
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00;
uint32_t tmpreg = 0x00, pinmask = 0x00;
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode));
assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin));
/*---------------------------- GPIO Mode Configuration -----------------------*/
currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);
if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00)
{
/* Check the parameters */
assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));
/* Output mode */
currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
}
/*---------------------------- GPIO CRL Configuration ------------------------*/
/* Configure the eight low port pins */
if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00)
{
tmpreg = GPIOx->CRL;
for (pinpos = 0x00; pinpos < 0x08; pinpos++)
{
pos = ((uint32_t)0x01) << pinpos;
/* Get the port pins position */
currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
if (currentpin == pos)
{
pos = pinpos << 2;
/* Clear the corresponding low control register bits */
pinmask = ((uint32_t)0x0F) << pos;
tmpreg &= ~pinmask;
/* Write the mode configuration in the corresponding bits */
tmpreg |= (currentmode << pos);
/* Reset the corresponding ODR bit */
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
{
GPIOx->BRR = (((uint32_t)0x01) << pinpos);
}
else
{
/* Set the corresponding ODR bit */
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
{
GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
}
}
}
}
GPIOx->CRL = tmpreg;
}
/*---------------------------- GPIO CRH Configuration ------------------------*/
/* Configure the eight high port pins */
if (GPIO_InitStruct->GPIO_Pin > 0x00FF)
{
tmpreg = GPIOx->CRH;
for (pinpos = 0x00; pinpos < 0x08; pinpos++)
{
pos = (((uint32_t)0x01) << (pinpos + 0x08));
/* Get the port pins position */
currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);
if (currentpin == pos)
{
pos = pinpos << 2;
/* Clear the corresponding high control register bits */
pinmask = ((uint32_t)0x0F) << pos;
tmpreg &= ~pinmask;
/* Write the mode configuration in the corresponding bits */
tmpreg |= (currentmode << pos);
/* Reset the corresponding ODR bit */
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
{
GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));
}
/* Set the corresponding ODR bit */
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
{
GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));
}
}
}
GPIOx->CRH = tmpreg;
}
}
该函数就是对GPIO的各个寄存器进行配置。
通过对时钟和GPIO的分析,我想大家已经对固件的逻辑有了一定的认识,从本质上讲,都是在配置寄存器,只是地址和值不同罢了,而固件库就是对寄存器配置的封装,便于开发者调用。
当然啦,本文是基于标准库分析,HAL库的逻辑也是一样的,只是HAL功能更完善,封装更彻底,后面也会详细分析HAL库的调用逻辑。
小贴士
1.assert_param
在STM32的固件库和提供的例程中,到处都可以见到assert_param()的使用。如果打开任何一个例程中的stm32f10x_conf.h文件,就可以看到实际上assert_param是一个宏定义;在固件库中,它的作用就是检测传递给函数的参数是否是有效的参数。
所谓有效的参数是指满足规定范围的参数,比如某个参数的取值范围只能是小于3的正整数,如果给出的参数大于3,则这个assert_param()可以在运行的程序调用到这个函数时报告错误,使程序员可以及时发现错误,而不必等到程序运行结果的错误而大费周折。这是一种常见的软件技术,可以在调试阶段帮助程序员快速地排除那些明显的错误。它确实在程序的运行上牺牲了效率(但只是在调试阶段),但在项目的开发上却帮助你提高了效率。
当你的项目开发成功,使用release模式编译之后,或在stm32f10x_conf.h文件中注释掉对USE_FULL_ASSERT的宏定义,所有的assert_param()检验都消失了,不会影响最终程序的运行效率。在执行assert_param()的检验时,如果发现参数出错,它会调用函数assert_failed()向程序员报告错误,在任何一个例程中的main.c中都有这个函数的模板,如下:
void assert_failed(uint8_t* file, uint32_t line)
{
while (1)
{}
}
你可以按照自己使用的环境需求,添加适当的语句输出错误的信息提示,或修改这个函数做出适当的错误处理。
1、STM32F10xD.LIB是DEBUG模式的库文件。
2、STM32F10xR.LIB是Release模式的库文件。
3、要选择DEBUG和RELEASE模式,需要修改stm32f10x_conf.h的内容。#define DEBUG 表示DEBUG模式,把该语句注释掉,则为RELEASE模式。
4、要选择DEBUG和RELEASE模式,也可以在Options,C/C++,Define里填入DEBUG的预定义。这样,就不需要修改stm32f10x_conf.h的内容。
5、如果把库加入项目,则不需要将ST的库源文件加入项目,比较方便。但是,库的选择要和DEBUG预定义对应。
2.I、 O 、__IO的含义
这是ST库里面的宏定义,定义如下:
#define __I volatile const /*!< defines 'read only' permissions */
#define __O volatile /*!< defines 'write only' permissions */
#define __IO volatile /*!< defines 'read / write' permissions */
显然,这三个宏定义都是用来替换成 volatile 和 const 的,所以我们先要了解 这两个关键字的作用:
__ I :输入口。既然是输入,那么寄存器的值就随时会外部修改,那就不能进行优化,每次都要重新从寄存器中读取。也不能写,即只读,不然就不是输入而是输出了。
__ O :输出口,也不能进行优化,不然你连续两次输出相同值,编译器认为没改变,就忽略了后面那
一次输出,假如外部在两次输出中间修改了值,那就影响输出。
__ IO:输入输出口,同上
为什么加下划线?
原因是:避免命名冲突
一般宏定义都是大写,但因为这里的字母比较少,所以再添加下划线来区分。这样一般都可以避免命名冲突问题,因为很少人这样命名,这样命名的人肯定知道这些是有什么用的。
经常写大工程时,都会发现老是命名冲突,要不是全局变量冲突,要不就是宏定义冲突,所以我们要尽量避免这些问题,不然出问题了都不知道问题在哪里。
欢迎访问我的网站
BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎
资源获取方式
1.关注公众号[嵌入式实验楼]
2.在公众号回复关键词[Cortex-M]获取资料提取码